package browser

import (
	"context"
	"encoding/json"
	"errors"
	"fmt"
	uurl "net/url"
	"os"
	"sync"
	"time"

	"gitee.com/baixudong/gospider/bs4"
	"gitee.com/baixudong/gospider/requests"
	"gitee.com/baixudong/gospider/tools"
	"github.com/tidwall/gjson"
)

type Page struct {
	host   string
	port   int
	id     string
	mouseX float64
	mouseY float64
	sync.Mutex
	ctx        context.Context
	cnl        context.CancelFunc
	webSock    *webSock
	preWebSock *webSock
	ReqCli     *requests.Client
	isMove     bool
	baseUrl    *uurl.URL
}
type PageOption struct {
	RequestOption requests.ClientOption
}

func (obj *Page) init() error {
	var err error
	if obj.webSock, err = newWebSock(obj.ctx, fmt.Sprintf("ws://%s:%d/devtools/page/%s",
		obj.host, obj.port, obj.id), fmt.Sprintf("http://%s:%d/", obj.host, obj.port),
	); err != nil {
		return err
	}
	obj.ReqCli = obj.webSock.reqCli
	if _, err = obj.webSock.send(obj.ctx, commend{
		Method: "Page.enable",
	}); err != nil {
		return err
	}
	if err = obj.AddScript(obj.ctx, stealth); err != nil {
		return err
	}
	return obj.AddScript(obj.ctx, stealth2)
}
func (obj *Page) AddScript(ctx context.Context, script string) error {
	_, err := obj.webSock.send(ctx, commend{
		Method: "Page.addScriptToEvaluateOnNewDocument",
		Params: map[string]any{
			"source": script,
		},
	})
	return err
}
func (obj *Page) Png(ctx context.Context, path string) error {
	rect, err := obj.GetLayoutMetrics(ctx)
	if err != nil {
		return err
	}
	rs, err := obj.webSock.send(ctx, commend{
		Method: "Page.captureScreenshot",
		Params: map[string]any{
			"format":                "png",
			"captureBeyondViewport": true,
			"clip": map[string]float64{
				"x":      rect.CssContentSize.X,
				"y":      rect.CssContentSize.Y,
				"width":  rect.CssContentSize.Width,
				"height": rect.CssContentSize.Height,
				"scale":  1,
			},
		},
	})
	if err != nil {
		return err
	}
	imgData, ok := rs.Result["data"].(string)
	if !ok {
		return errors.New("not img data")
	}
	imgCon, err := tools.Base64Decode(imgData)
	if err != nil {
		return err
	}
	return os.WriteFile(path, imgCon, 0777)
}

type VisualViewport struct {
	OffsetX      float64 `json:"offsetX"`
	OffsetY      float64 `json:"offsetY"`
	PageX        float64 `json:"pageX"`
	PageY        float64 `json:"pageY"`
	ClientWidth  float64 `json:"clientWidth"`
	ClientHeight float64 `json:"clientHeight"`
	Scale        float64 `json:"scale"`
	Zoom         float64 `json:"zoom"`
}
type Rect struct {
	X      float64 `json:"x"`
	Y      float64 `json:"y"`
	Height float64 `json:"height"`
	Width  float64 `json:"width"`
}
type LayoutMetrics struct {
	CssLayoutViewport VisualViewport `json:"cssLayoutViewport"`
	CssVisualViewport VisualViewport `json:"cssVisualViewport"`
	CssContentSize    Rect           `json:"cssContentSize"`
}

func (obj *Page) GetLayoutMetrics(ctx context.Context) (LayoutMetrics, error) {
	rs, err := obj.webSock.send(ctx, commend{
		Method: "Page.getLayoutMetrics",
		Params: map[string]any{},
	})
	var result LayoutMetrics
	if err != nil {
		return result, err
	}
	return result, tools.Map2struct(rs.Result, &result)
}
func (obj *Page) Reload(ctx context.Context) error {
	_, err := obj.webSock.send(ctx, commend{
		Method: "Page.reload",
		Params: map[string]any{},
	})
	return err
}
func (obj *Page) GoTo(ctx context.Context, url string) error {
	obj.Lock()
	defer obj.Unlock()
	var err error
	if obj.baseUrl, err = uurl.Parse(url); err != nil {
		return err
	}
	_, err = obj.webSock.send(ctx, commend{
		Method: "Page.navigate",
		Params: map[string]any{
			"url": url,
		},
	}, "Page.frameStoppedLoading")
	return err
}
func (obj *Page) Eval(ctx context.Context, expression string, params map[string]any) (gjson.Result, error) {
	obj.Lock()
	defer obj.Unlock()
	var value string
	if params != nil {
		con, err := json.Marshal(params)
		if err != nil {
			return gjson.Result{}, err
		}
		value = tools.BytesToString(con)
	}
	rs, err := obj.webSock.send(ctx, commend{
		Method: "Runtime.evaluate",
		Params: map[string]any{
			"expression":    fmt.Sprintf(`(%s)(%s)`, expression, value),
			"returnByValue": true,
		},
	})
	return tools.Any2json(rs.Result), err
}
func (obj *Page) Close(ctx context.Context) error {
	_, err := obj.preWebSock.send(ctx, commend{
		Method: "Target.closeTarget",
		Params: map[string]any{
			"targetId": obj.id,
		},
	})
	if err != nil {
		return err
	}
	obj.cnl()
	return nil
}
func (obj *Page) Done() <-chan struct{} {
	return obj.webSock.ctx.Done()
}
func (obj *Page) Route(ctx context.Context, routeFunc func(context.Context, *Route)) error {
	obj.webSock.routeFunc = routeFunc
	var err error
	if obj.webSock.routeFunc != nil {
		_, err = obj.webSock.send(ctx, commend{
			Method: "Fetch.enable",
		})
	} else {
		_, err = obj.webSock.send(ctx, commend{
			Method: "Fetch.disable",
		})
	}
	return err
}
func (obj *Page) rootId(ctx context.Context) (int64, error) {
	rs, err := obj.webSock.send(ctx, commend{
		Method: "DOM.getDocument",
		Params: map[string]any{
			"depth": 0,
		},
	})
	if err != nil {
		return 0, err
	}
	return tools.Any2json(rs.Result["root"]).Get("nodeId").Int(), err
}
func (obj *Page) Html(ctx context.Context, contents ...string) (*bs4.Client, error) {
	rootId, err := obj.rootId(ctx)
	if err != nil {
		return nil, err
	}
	if len(contents) > 0 {
		return nil, obj.setHtml(ctx, rootId, contents[0])
	}
	return obj.html(ctx, rootId)
}
func (obj *Page) setHtml(ctx context.Context, nodeId int64, content string) error {
	_, err := obj.webSock.send(ctx, commend{
		Method: "DOM.setOuterHTML",
		Params: map[string]any{
			"nodeId":    nodeId,
			"outerHTML": content,
		},
	})
	return err
}
func (obj *Page) html(ctx context.Context, nodeId int64) (*bs4.Client, error) {
	rs, err := obj.webSock.send(ctx, commend{
		Method: "DOM.getOuterHTML",
		Params: map[string]any{
			"nodeId": nodeId,
		},
	})
	if err != nil {
		return nil, err
	}
	html := bs4.NewClient(rs.Result["outerHTML"].(string), obj.baseUrl.String())
	iframes := html.Finds("iframe")
	if len(iframes) > 0 {
		pageFrams, err := obj.Iframes(ctx)
		if err != nil {
			return nil, err
		}
		for _, ifram := range iframes {
			ju, err := tools.UrlJoin(obj.baseUrl.String(), ifram.Get("src"))
			if err != nil {
				return nil, err
			}
			frame, ok := pageFrams[ju]
			if ok {
				dh, err := frame.Html(ctx)
				if err != nil {
					return nil, err
				}
				ifram.Html(dh.Html())
			}
		}
	}
	return html, nil
}
func (obj *Page) WaitSelector(preCtx context.Context, selector string, timeouts ...int64) (*Dom, error) {
	if preCtx == nil {
		preCtx = context.TODO()
	}
	var timeout int64
	if len(timeouts) > 0 {
		timeout = timeouts[0]
	}
	if timeout == 0 {
		timeout = 30
	}
	ctx, cnl := context.WithTimeout(preCtx, time.Second*time.Duration(timeout))
	defer cnl()
	for {
		dom, err := obj.QuerySelector(ctx, selector)
		if err != nil {
			return nil, err
		}
		if dom != nil {
			return dom, nil
		}
		time.Sleep(time.Millisecond * 500)
	}
}
func (obj *Page) QuerySelector(ctx context.Context, selector string) (*Dom, error) {
	rootId, err := obj.rootId(ctx)
	if err != nil {
		return nil, err
	}
	rs, err := obj.webSock.send(ctx, commend{
		Method: "DOM.querySelector",
		Params: map[string]any{
			"nodeId":   rootId,
			"selector": selector,
		},
	})
	if err != nil {
		return nil, err
	}
	nodeId, ok := rs.Result["nodeId"].(float64)
	if !ok {
		return nil, errors.New("not found")
	}
	if nodeId == 0 {
		return nil, nil
	}
	dom := Dom{
		baseUrl: obj.baseUrl,
		webSock: obj.webSock,
		nodeId:  int64(nodeId),
	}
	return &dom, nil
}
func (obj *Page) QuerySelectorAll(ctx context.Context, selector string) ([]*Dom, error) {
	rootId, err := obj.rootId(ctx)
	if err != nil {
		return nil, err
	}
	rs, err := obj.webSock.send(ctx, commend{
		Method: "DOM.querySelectorAll",
		Params: map[string]any{
			"nodeId":   rootId,
			"selector": selector,
		},
	})
	if err != nil {
		return nil, err
	}
	doms := []*Dom{}
	for _, nodeId := range tools.Any2json(rs.Result["nodeIds"]).Array() {
		doms = append(doms, &Dom{
			baseUrl: obj.baseUrl,
			webSock: obj.webSock,
			nodeId:  nodeId.Int(),
		})
	}
	return doms, nil
}
func (obj *Page) baseMove(ctx context.Context, point Point, kind int, steps ...int) error {
	if !obj.isMove {
		obj.mouseX = point.X
		obj.mouseY = point.Y
		obj.isMove = true
		return nil
	}
	var step int
	if len(steps) > 0 {
		step = steps[0]
	}
	if step < 1 {
		step = 1
	}
	for _, poi := range tools.GetTrack(
		[2]float64{obj.mouseX, obj.mouseY},
		[2]float64{point.X, point.Y},
		float64(step),
	) {
		switch kind {
		case 0:
			if err := obj.move(ctx, Point{
				X: poi[0],
				Y: poi[1],
			}); err != nil {
				return err
			}
		case 1:
			if err := obj.touchMove(ctx, Point{
				X: poi[0],
				Y: poi[1],
			}); err != nil {
				return err
			}
		default:
			return errors.New("not found kind")
		}
	}
	obj.isMove = true
	return nil
}

func (obj *Page) Move(ctx context.Context, point Point, steps ...int) error {
	return obj.baseMove(ctx, point, 0, steps...)
}
func (obj *Page) move(ctx context.Context, point Point) error {
	_, err := obj.webSock.send(ctx, commend{
		Method: "Input.dispatchMouseEvent",
		Params: map[string]any{
			"type": "mouseMoved",
			"x":    point.X,
			"y":    point.Y,
		},
	})
	if err != nil {
		return err
	}
	obj.mouseX = point.X
	obj.mouseY = point.Y
	return nil
}

func (obj *Page) Down(ctx context.Context, point Point) error {
	_, err := obj.webSock.send(ctx, commend{
		Method: "Input.dispatchMouseEvent",
		Params: map[string]any{
			"type":   "mousePressed",
			"button": "left",
			"x":      point.X,
			"y":      point.Y,
		},
	})
	if err != nil {
		return err
	}
	obj.mouseX = point.X
	obj.mouseY = point.Y
	obj.isMove = true
	return err
}
func (obj *Page) Up(ctx context.Context) error {
	_, err := obj.webSock.send(ctx, commend{
		Method: "Input.dispatchMouseEvent",
		Params: map[string]any{
			"type":   "mouseReleased",
			"button": "left",
			"x":      obj.mouseX,
			"y":      obj.mouseY,
		},
	})
	return err
}
func (obj *Page) Click(ctx context.Context, point Point) error {
	if err := obj.Down(ctx, point); err != nil {
		return err
	}
	return obj.Up(ctx)
}

func (obj *Page) TouchMove(ctx context.Context, point Point, steps ...int) error {
	return obj.baseMove(ctx, point, 1, steps...)
}
func (obj *Page) touchMove(ctx context.Context, point Point) error {
	_, err := obj.webSock.send(ctx, commend{
		Method: "Input.dispatchTouchEvent",
		Params: map[string]any{
			"type": "touchMove",
			"touchPoints": []Point{
				{
					X: point.X,
					Y: point.Y,
				},
			},
		},
	})
	if err != nil {
		return err
	}
	obj.mouseX = point.X
	obj.mouseY = point.Y
	return nil
}
func (obj *Page) TouchDown(ctx context.Context, point Point) error {
	_, err := obj.webSock.send(ctx, commend{
		Method: "Input.dispatchTouchEvent",
		Params: map[string]any{
			"type": "touchStart",
			"touchPoints": []Point{
				point,
			},
		},
	})
	if err != nil {
		return err
	}
	obj.mouseX = point.X
	obj.mouseY = point.Y
	return nil
}
func (obj *Page) TouchUp(ctx context.Context) error {
	_, err := obj.webSock.send(ctx, commend{
		Method: "Input.dispatchTouchEvent",
		Params: map[string]any{
			"type":        "touchEnd",
			"touchPoints": []Point{},
		},
	})
	return err
}

func (obj *Page) SetDevice(ctx context.Context, device Device) error {
	if err := obj.SetUserAgent(ctx, device.UserAgent); err != nil {
		return err
	}
	if err := obj.SetTouch(ctx, device.HasTouch); err != nil {
		return err
	}
	return obj.SetDeviceMetrics(ctx, device)
}

func (obj *Page) SetUserAgent(ctx context.Context, userAgent string) error {
	_, err := obj.webSock.send(ctx, commend{
		Method: "Emulation.setUserAgentOverride",
		Params: map[string]any{
			"userAgent": userAgent,
		},
	})
	return err
}

func (obj *Page) SetDeviceMetrics(ctx context.Context, device Device) error {
	_, err := obj.webSock.send(ctx, commend{
		Method: "Emulation.setDeviceMetricsOverride",
		Params: map[string]any{
			"width":             device.Viewport.Width,
			"height":            device.Viewport.Height,
			"deviceScaleFactor": device.DeviceScaleFactor,
			"mobile":            device.IsMobile,
		},
	})
	return err
}
func (obj *Page) SetTouch(ctx context.Context, hasTouch bool) error {
	_, err := obj.webSock.send(ctx, commend{
		Method: "Emulation.setTouchEmulationEnabled",
		Params: map[string]any{
			"enabled": hasTouch,
		},
	})
	return err
}

type Cookie struct {
	Name         string  `json:"name,omitempty"`  //必填
	Value        string  `json:"value,omitempty"` //必填
	Url          string  `json:"url,omitempty"`
	Domain       string  `json:"domain,omitempty"` //必填
	Path         string  `json:"path,omitempty"`
	Secure       bool    `json:"secure,omitempty"`
	HttpOnly     bool    `json:"httpOnly,omitempty"`
	SameSite     string  `json:"sameSite,omitempty"`
	Expires      float64 `json:"expires,omitempty"`
	Priority     string  `json:"priority,omitempty"`
	SameParty    bool    `json:"sameParty,omitempty"`
	SourceScheme string  `json:"sourceScheme,omitempty"`
	SourcePort   int     `json:"sourcePort,omitempty"`
	PartitionKey int     `json:"partitionKey,omitempty"`
	Session      bool    `json:"session,omitempty"`
	Size         int64   `json:"size,omitempty"`
}

func (obj *Page) SetCookies(ctx context.Context, cookies ...Cookie) error {
	if len(cookies) == 0 {
		return nil
	}
	var err error
	for i := 0; i < len(cookies); i++ {
		if cookies[i].Domain == "" {
			if cookies[i].Url != "" {
				us, err := uurl.Parse(cookies[i].Url)
				if err != nil {
					return err
				}
				cookies[i].Domain = us.Hostname()
			} else if obj.baseUrl != nil {
				cookies[i].Domain = obj.baseUrl.Hostname()
			}
		}
	}
	_, err = obj.webSock.send(ctx, commend{
		Method: "Network.setCookies",
		Params: map[string]any{
			"cookies": cookies,
		},
	})
	return err
}

func (obj *Page) Iframes(ctx context.Context) (map[string]*Iframe, error) {
	rs, err := obj.webSock.send(ctx, commend{
		Method: "Page.getFrameTree",
	})
	if err != nil {
		return nil, err
	}
	results := map[string]*Iframe{}
	for _, ifram := range tools.Any2json(rs.Result).Get("frameTree.childFrames").Array() {
		frameId := ifram.Get("frame.id").String()
		frameUrl := ifram.Get("frame.url").String()
		results[frameUrl] = &Iframe{
			iframeId:  frameId,
			iframeUrl: frameUrl,
			webSock:   obj.webSock,
		}
	}
	return results, nil
}

//	func (obj *Page) Test() {
//		frams, err := obj.Iframes(nil)
//		if err != nil {
//			log.Panic(err)
//		}
//		rs, err := obj.webSock.send(nil, commend{
//			Method: "Page.getResourceContent",
//			Params: map[string]any{
//				"frameId": frams[0].iframeId,
//				"url":     frams[0].iframeUrl,
//			},
//		})
//		if err != nil {
//			log.Panic(err)
//		}
//		log.Print(rs)
//		log.Print(tools.Any2json(rs.Result).Get("content"))
//	}
func (obj *Page) GetCookies(ctx context.Context, urls ...string) ([]Cookie, error) {
	var err error
	var rs recvData
	if len(urls) == 0 {
		rs, err = obj.webSock.send(ctx, commend{
			Method: "Network.getCookies",
		})
	} else {
		rs, err = obj.webSock.send(ctx, commend{
			Method: "Network.getCookies",
			Params: map[string]any{
				"urls": urls,
			},
		})
	}
	result := []Cookie{}
	if err != nil {
		return result, err
	}
	jsonData := tools.Any2json(rs.Result)
	for _, cookie := range jsonData.Get("cookies").Array() {
		var cook Cookie
		if err = json.Unmarshal(tools.StringToBytes(cookie.Raw), &cook); err != nil {
			return result, err
		}
		result = append(result, cook)
	}
	return result, nil
}
